package info.motteke.io;

import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.Writer;
import java.util.Locale;

/**
 * インデントを追加できるPrintWriterです。
 *
 * @author YWak
 */
public class IndentablePrintWriter extends PrintWriter {

    /**
     * デフォルトのインデントです。
     */
    private static final String DEFAULT_INDENT = "  ";

    /**
     * デフォルトのautoFlushの値です。
     */
    private static final boolean DEFAULT_AUTO_FLUSH = true;

    /**
     * インデント一つ分の文字列です。
     */
    private String unitIndent;

    /**
     * 現在のインデントのレベルです。
     */
    private int indentLevel = 0;

    /**
     * 自動的にflushを呼び出すかどうかです。
     */
    private boolean autoFlush;

    /**
     * flushを待っている文字列を格納するバッファです。
     */
    private StringBuilder buf;

    /**
     * 現在文字列を追加する際にインデントを挿入するかを判断します。
     */
    private boolean insertIndent = true;

    /**
     * コンストラクタです。
     * デフォルトのインデントと、デフォルトのautoFlushを適用します。
     * @param os 書き込み先のストリーム
     */
    public IndentablePrintWriter(OutputStream os) {
        this(DEFAULT_INDENT, os);
    }

    public IndentablePrintWriter(Writer writer) {
        this(DEFAULT_INDENT, writer);
    }

    public IndentablePrintWriter(String unitIndent, OutputStream os) {
        this(unitIndent, new OutputStreamWriter(os), DEFAULT_AUTO_FLUSH);
    }

    public IndentablePrintWriter(String unitIndent, Writer writer) {
        this(unitIndent, writer, DEFAULT_AUTO_FLUSH);
    }

    public IndentablePrintWriter(String unitIndent, OutputStream os, boolean autoFlush) {
        this(unitIndent, new OutputStreamWriter(os), autoFlush);
    }

    public IndentablePrintWriter(String unitIndent, Writer writer, boolean autoFlush) {
        super(writer);
        this.unitIndent = unitIndent;
        this.autoFlush = autoFlush;
        buf = new StringBuilder();
    }

    @Override
    public IndentablePrintWriter append(char c) {
        appendString(String.valueOf(c));
        return this;
    }

    @Override
    public IndentablePrintWriter append(CharSequence csq, int start, int end) {
        appendString(String.valueOf(csq).substring(start, end));
        return this;
    }

    @Override
    public IndentablePrintWriter append(CharSequence csq) {
        appendString(String.valueOf(csq));
        return this;
    }

    @Override
    public IndentablePrintWriter format(Locale l, String format, Object... args) {
        appendString(String.format(l, format, args));

        if (autoFlush) {
            flush();
        }

        return this;
    }

    @Override
    public PrintWriter format(String format, Object... args) {
        appendString(String.format(format, args));

        if (autoFlush) {
            flush();
        }

        return this;
    }

    @Override
    public void print(boolean b) {
        appendString(String.valueOf(b));
    }

    @Override
    public void print(char c) {
        appendString(String.valueOf(c));
    }

    @Override
    public void print(char[] s) {
        appendString(String.valueOf(s));
    }

    @Override
    public void print(double d) {
        appendString(String.valueOf(d));
    }

    @Override
    public void print(float f) {
        appendString(String.valueOf(f));
    }

    @Override
    public void print(int i) {
        appendString(String.valueOf(i));
    }

    @Override
    public void print(long l) {
        appendString(String.valueOf(l));
    }

    @Override
    public void print(Object obj) {
        appendString(String.valueOf(obj));
    }

    @Override
    public void print(String s) {
        appendString(String.valueOf(s));
    }

    @Override
    public IndentablePrintWriter printf(Locale l, String format, Object... args) {
        appendString(String.format(l, format, args));

        if (autoFlush) {
            flush();
        }
        return this;
    }

    @Override
    public PrintWriter printf(String format, Object... args) {
        appendString(String.format(format, args));

        if (autoFlush) {
            flush();
        }
        return this;
    }

    @Override
    public void println() {
        appendString(System.getProperty("line.separator"));
        if (autoFlush) {
            flush();
        }
    }

    @Override
    public void println(boolean x) {
        print(x);
        println();
    }

    @Override
    public void println(char x) {
        print(x);
        println();
    }

    @Override
    public void println(char[] x) {
        print(x);
        println();
    }

    @Override
    public void println(double x) {
        print(x);
        println();
    }

    @Override
    public void println(float x) {
        print(x);
        println();
    }

    @Override
    public void println(int x) {
        print(x);
        println();
    }

    @Override
    public void println(long x) {
        print(x);
        println();
    }

    @Override
    public void println(Object x) {
        print(x);
        println();
    }

    @Override
    public void println(String x) {
        print(x);
        println();
    }

    @Override
    public void write(char[] buf, int off, int len) {
        appendString(new String(buf, off, len));
    }

    @Override
    public void write(char[] buf) {
        appendString(new String(buf));
    }

    @Override
    public void write(int c) {
        appendString(Character.toString((char) c));
    }

    @Override
    public void write(String s, int off, int len) {
        appendString(s.substring(off, off + len));
    }

    @Override
    public void write(String s) {
        appendString(s);
    }

    @Override
    public void close() {
        flush();
        super.close();
    }

    @Override
    public void flush() {
        try{
            out.append(buf);
            out.flush();
            buf = new StringBuilder();
        } catch (IOException e) {
            setError();
        }
    }

    /**
     * インデントレベルを上げます。
     * レベルを上げる前に、{@linkplain #flush()}が呼ばれます。
     */
    public void indent(){
        flush();
        indentLevel++;
    }

    /**
     * インデントレベルを下げます。
     * レベルを下げる前に、{@linkplain #flush()}が呼ばれます。
     */
    public void dedent(){
        flush();
        indentLevel--;
    }

    /**
     * 文字列をバッファに格納します。
     *
     * @param string 格納する文字列
     */
    private void appendString(String string) {
        String br = System.getProperty("line.separator");
        int brLength = br.length();
        StringBuilder indent = new StringBuilder();

        //レベルにあったインデントを生成します。
        for(int i = 0; i < indentLevel; i++){
            indent.append(unitIndent);
        }

        for(int i = 0, length = string.length(); i < length; i++){
            // 必要があればインデントを追加する
            if(insertIndent){
                buf.append(indent);
                insertIndent = false;
            }

            char c = string.charAt(i);

            // 改行コードの可能性がある場合を調べる。
            // KMP法あたりに乗り換える方がいいかも知れない。
            for(int j = 0; j < brLength; j++){
                int index = i + j;
                if(!(index < length && string.charAt(index) == br.charAt(j))){
                    buf.append(c);
                    break;
                }
            }
            // 改行コードであれば改行コードを追加し、インデントを付加するマークを付ける。
            // また、まだ読まれていないコードの分を読み進める。
            buf.append(br);
            insertIndent = true;
            i += brLength - 1;
        }
    }
}
